#include <stdlib.h>

#ifdef UNIX
# include <unistd.h>
# include <signal.h>
#endif

#ifdef NCURSES
# include "host/ncurses.h"
#endif

#include "core.h"
#include "loaders.h"
#include "log.h"
#include "memmap.h"
#include "cpu/int.h"
#include "cpu/mips.h"
#include "fs/metafs.h"
#include "gfx/display.h"
#include "gfx/dlint.h"
#include "hle/__sceAudio.h"
#include "host/host.h"

CoreState coreState = CORE_RUNNING;

u32 bootAddr = 0;

#ifndef NO_THREAD
Mutex csMtx; /* core state mutex */

struct GfxThStackElem
{
    GfxThType type;
    u32 arg;
    struct GfxThStackElem *next;
};

typedef struct GfxThStackElem GfxThStackElem;

Thread gfxTh, intTh, sndTh;

Condition            *gfxTh_start;
volatile u8           gfxTh_startv = 0;
Mutex                 gfxTh_condMtx;
Mutex                 gfxTh_stackMtx;
GfxThStackElem       *gfxTh_firstElem = NULL;
volatile u8           gfxTh_drawSync = 0;

Mutex                 intTh_mtx;
Condition            *intTh_ready;
volatile IntThCbState intTh_state = INTTH_READY;
volatile u32          intTh_cbId;
volatile u8           intTh_cbHasArg;
volatile u32          intTh_cbArg;
volatile WaitType     intTh_resumeWait = WAITTYPE_NONE;
volatile u8           intTh_vsync = 0;

Condition            *sndTh_cond;
Mutex                 sndTh_mtx;
#endif

u8 isDbg = 0;

void core_start(const char *file)
{
#ifdef UNIX
    signal(SIGINT,  core_stop);
    signal(SIGHUP,  core_stop);
    signal(SIGTERM, core_stop);
#endif

    coreState = CORE_RUNNING;

#ifdef NCURSES
    if (isDbg)
        ncurses_init();
#endif

    mfs_init();
    memMap_init();
    cpu_reset();
    display_init();

    __KernelInit();

    if (host_init() == -1)
        _log(ERR, CORE, "Host initialization failed! Exiting.");
    else
    {
        if ((bootAddr ? load_raw(file, bootAddr) : load_rom(file)) == LOAD_OK)
        {
#ifdef NCURSES
            if (isDbg)
                core_ncursesLoop();
            else
            {
#endif

#ifdef NO_THREAD
            core_loop();
#else
            cs_init(&csMtx, "Core State");
            cs_init(&gfxTh_condMtx, "Gfx Thread Condition");
            cs_init(&gfxTh_stackMtx, "Gfx Thread Stack");
            cs_init(&intTh_mtx, "Int Thread");
            cs_init(&sndTh_mtx, "Snd Thread");

            dl_initTh();

            intTh_ready = thread_cond_init(0);
            gfxTh_start = thread_cond_init(100);
            sndTh_cond  = thread_cond_init(50);

            thread_init(&gfxTh, &core_gfxTh, NULL); /* thread for window, graphics, .. */
            thread_init(&intTh, &core_intTh, NULL); /* thread for CPU interpreting, HLE, .. */
#ifndef NO_AUDIO
            thread_init(&sndTh, &core_sndTh, NULL); /* thread for audio */
#endif

            /* wait while the graphics thread did not quit: it's the one that handle events */
            thread_waitForDeath(&gfxTh);

            /* stop the other threads & wait for them to end */
            _log(INF, CORE, "Waiting for the interpreting thread to end..");
            core_stop(); /* Stop core if not already done */
            thread_waitForDeath(&intTh);
#ifndef NO_AUDIO
            _log(INF, CORE, "Waiting for the audio thread to end..");
            thread_waitForDeath(&sndTh);
#endif
            _log(INF, CORE, "Ended");

            thread_cond_free(sndTh_cond);
            thread_cond_free(gfxTh_start);
            thread_cond_free(intTh_ready);

            dl_freeTh();

            cs_free(&sndTh_mtx);
            cs_free(&intTh_mtx);
            cs_free(&gfxTh_stackMtx);
            cs_free(&gfxTh_condMtx);
            cs_free(&csMtx);
#endif

#ifdef NCURSES
            }
#endif
        }
        host_free();
    }

    __KernelShutdown();

    display_free();
    memMap_free();
    mfs_free();
    
#ifdef NCURSES
    if (isDbg)
        ncurses_quit();
#endif
}

void core_stop()
{
    cs_enter(&csMtx);
    coreState = CORE_POWERDOWN;
    cs_leave(&csMtx);
}

void core_wait()
{
    cs_enter(&csMtx);
    if (coreState != CORE_POWERDOWN)
        coreState = CORE_WAITING;
    cs_leave(&csMtx);
}

void core_run()
{
    cs_enter(&csMtx);
    if (coreState != CORE_POWERDOWN)
        coreState = CORE_RUNNING;
    cs_leave(&csMtx);
}

Timer drawTimer;
s32 core_winInit()
{
    if (host_winInit() == -1)
        return -1;

    display_dispInit();
    drawTimer = host_initTimer(17, 1);
    return 0;
}

void core_winQuit()
{
    display_dispFree();

    host_winQuit();
}

u8 core_syncHost()
{
    host_syncEvents();
    if (host_isUpdateDbgLvl())
    {
        debug_level++;
        if (debug_level == NUMDBGLVL)
            debug_level = 0;
    }
    if (host_isQuit())
        core_stop();

    if (core_getState() == CORE_POWERDOWN)
    {
        core_winQuit();
#ifndef NO_THREAD
        core_clearGfxThStack();
#endif
        return 1;
    }

    if (host_testTimer(&drawTimer))
    {
#ifdef NO_THREAD
        display_sync(0);
        __KernelVsync();
#else
        display_syncFromTh(0);
        cs_enter(&intTh_mtx);
        intTh_vsync = 1;
        cs_leave(&intTh_mtx);
#endif
    }
    return 0;
}

#ifdef NCURSES
void core_ncursesLoop()
{
    u8 run = 0;

    ncurses_setPC(cpu.pc);

    if (core_winInit() == -1) {
        _log(ERR, CORE, "Display initialization failed! Exiting.");
        return;
    }

    for (;;)
    {
        if (!run)
            ncurses_show();

        switch (ncurses_key())
        {
        case 0:
            core_winQuit();
            return;

        case 1:
            if (!run) {
                ncurses_setPC(cpu.pc);
                cpu_singleStep();
            }
            break;

        case 2:
            run ^= 1;
            ncurses_delay(run);
        }

        if (run)
            cpu_singleStep();
    }
}
#endif

#ifdef NO_THREAD
void core_loop()
{
    if (core_winInit() == -1) {
        _log(ERR, CORE, "Display initialization failed! Exiting.");
        return;
    }

    for (;;)
    {
        if (core_syncHost())
            return;

        if (core_getState() == CORE_RUNNING)
            cpu_singleStep();
    }
}
#else

CoreThread core_getCurThread()
{
    cs_enter(&csMtx);
    if (thread_isActual(&intTh)) {
        cs_leave(&csMtx);
        return THREAD_INTERPRETER;
    }
    else if (thread_isActual(&gfxTh)) {
        cs_leave(&csMtx);
        return THREAD_GRAPHICS;
    }
    else if (thread_isActual(&sndTh)) {
        cs_leave(&csMtx);
        return THREAD_AUDIO;
    }
    cs_leave(&csMtx);
    return THREAD_NONE;
}

/*******************
 * Graphics thread *
 *******************/
void core_addToGfxThStack(GfxThType type, u32 arg)
{
    GfxThStackElem *new;

    cs_enter(&gfxTh_stackMtx);
    for (new = gfxTh_firstElem; new != NULL; new = new->next)
    {
        if (new->arg == arg) { /* Display List already in stack! */
            cs_leave(&gfxTh_stackMtx);
            return;
        }
    }
    cs_leave(&gfxTh_stackMtx);
    
    cs_enter(&gfxTh_condMtx);

    /* Add element to stack */
    cs_enter(&gfxTh_stackMtx);
    new = malloc(sizeof(GfxThStackElem));
    new->type = type;
    new->arg = arg;
    new->next = NULL;
    if (gfxTh_firstElem)
    {
        GfxThStackElem *nextElem = gfxTh_firstElem;
        while (nextElem->next != NULL)
            nextElem = nextElem->next;
        nextElem->next = new;
    }
    else
        gfxTh_firstElem = new;

    cs_leave(&gfxTh_stackMtx);

    gfxTh_startv = 1;
    thread_cond_signal(gfxTh_start);
    cs_leave(&gfxTh_condMtx);
}

void core_delFromGfxThStack()
{
    GfxThStackElem *newStart = gfxTh_firstElem->next;

    free(gfxTh_firstElem);
    gfxTh_firstElem = newStart;
}

u8 core_inGfxThStack(GfxThType type)
{
    u8 ret = 0;
    GfxThStackElem *cur;
    cs_enter(&gfxTh_stackMtx);
    cur = gfxTh_firstElem;
    while (cur)
    {
        if (cur->type == type) {
            ret = 1;
            break;
        }
        cur = cur->next;
    }
    cs_leave(&gfxTh_stackMtx);
    return ret;
}

void core_clearGfxThStack()
{
    cs_enter(&gfxTh_stackMtx);
    while (gfxTh_firstElem != NULL)
        core_delFromGfxThStack();
    cs_leave(&gfxTh_stackMtx);
}

void core_drawSync()
{
    cs_enter(&gfxTh_stackMtx);
    gfxTh_drawSync = 1;
    cs_leave(&gfxTh_stackMtx);
}

TH_TYPE core_gfxTh(void *tharg)
{
    GfxThType type;
    u32 arg;

    if (core_winInit() == -1) {
        _log(ERR, CORE, "Display initialization failed! Exiting.");
        return (TH_RET)tharg;
    }

    for (;;)
    {
        cs_enter(&gfxTh_condMtx);
        if (!gfxTh_startv)
        {
            while (thread_cond_timedWait(gfxTh_start, &gfxTh_condMtx) != COND_MET)
            {
                if (core_syncHost()) {
                    cs_leave(&gfxTh_condMtx);
                    return (TH_RET)tharg;
                }
                if (gfxTh_startv)
                    break;
            }
        }

        gfxTh_startv = 0;
        cs_leave(&gfxTh_condMtx);

        while (gfxTh_firstElem)
        {
            /* Get type and argument from FIFO */
            cs_enter(&gfxTh_stackMtx);
            type = gfxTh_firstElem->type;
            arg = gfxTh_firstElem->arg;
            core_delFromGfxThStack();
            cs_leave(&gfxTh_stackMtx);

            if (core_syncHost())
                return (TH_RET)tharg;

            switch (type)
            {
            case GFXTH_SETFB:
                display_setFbTh();
                break;

            case GFXTH_FINISH:
                display_syncFromTh(arg);
                host_updateTimerTime(&drawTimer);
                break;

            case GFXTH_DLINT:
                dl_interpretTh(arg);
                break;
            }

            if (!core_inGfxThStack(GFXTH_DLINT))
            {
                cs_enter(&gfxTh_stackMtx);
                if (gfxTh_drawSync)
                {
                    gfxTh_drawSync = 0;
                    cs_leave(&gfxTh_stackMtx);
                    core_callResumeThreads(WAITTYPE_DRAWSYNC);
                }
                else
                    cs_leave(&gfxTh_stackMtx);
            }
        }
    }
}

/***********************
 * Interpreting thread *
 **********************/
void core_callResumeThreads(WaitType type)
{
    core_waitForResumeEnd();
    cs_enter(&intTh_mtx);
    intTh_resumeWait = type;
    cs_leave(&intTh_mtx);
}

void core_waitForResumeEnd()
{
    cs_enter(&intTh_mtx);
    while (intTh_resumeWait != WAITTYPE_NONE)
    {
        cs_leave(&intTh_mtx);
        if (core_getState() == CORE_POWERDOWN)
            return;
        cs_enter(&intTh_mtx);
    }
    cs_leave(&intTh_mtx);
}

TH_TYPE core_intTh(void *arg)
{
    CoreState state;
    while ((state = core_getState()) != CORE_POWERDOWN)
    {
        cs_enter(&intTh_mtx);
        if (intTh_state == INTTH_MUSTSTART)
        {
            if (intTh_cbHasArg)
                __KernelCallCallbackWithArg(intTh_cbId, intTh_cbArg);
            else
                __KernelCallCallback(intTh_cbId);
            core_run();
            intTh_state = INTTH_RUNNING;
        }

        if (intTh_resumeWait != WAITTYPE_NONE) {
            __KernelResumeThreads(intTh_resumeWait, 0, RESUMETYPE_NONE);
            intTh_resumeWait = WAITTYPE_NONE;
        }

        if (intTh_vsync == 1) {
            intTh_vsync = 0;
            __KernelVsync();
        }
        cs_leave(&intTh_mtx);

        if (state == CORE_WAITING)
        {
            if (__KernelReadyThreads()
             || __KernelResumeDelayedThreads())
                __KernelReSchedule();
        }
        else
            cpu_singleStep();
    }
    return (TH_RET)arg;
}

/****************
 * Sound thread *
 ****************/
TH_TYPE core_sndTh(void *arg)
{
    __AudioInit();
    cs_enter(&sndTh_mtx);
    do
    {
        host_audioUpdate();
        cs_leave(&sndTh_mtx); /* In case the interpreter thread is trying to lock the mutex at the same time */
        core_callResumeThreads(WAITTYPE_AUDIOCHANNEL);
        cs_enter(&sndTh_mtx);
    } while (thread_cond_timedWait(sndTh_cond, &sndTh_mtx) != COND_MET
             && core_getState() != CORE_POWERDOWN);
    cs_leave(&sndTh_mtx);
    __AudioShutdown();
    return (TH_RET)arg;
}
#endif

CoreState core_getState()
{
    CoreState state;
    cs_enter(&csMtx);
    state = coreState;
    cs_leave(&csMtx);
    return state;
}

